package com.googlecode.compliancechecker;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Core component that checks whether a given byte code stream is compliant with
 * a certain android api level.
 * 
 * @author Marc-Christian Schulze
 * 
 */
public class ComplianceChecker {

	private AndroidJar				androidJar;
	private List<String>			scanPackages;
	private List<TypeReference>	missingReferences;
	private ReferenceVerifier		verifier;

	public ComplianceChecker(File androidHome, int apiLevel, List<String> packagesToScan) throws IOException {

		androidJar = new AndroidJar(androidHome, apiLevel);
		verifier = new ReferenceVerifier(androidJar);

		missingReferences = new ArrayList<TypeReference>();
		scanPackages = packagesToScan;
	}

	public List<TypeReference> getMissingReferences() {
		return missingReferences;
	}

	public boolean isCompliant(InputStream classFileStream) throws IOException {
		Set<TypeReference> references = collectReferences(classFileStream);
		boolean overallSuccess = true;
		for (TypeReference ref : references) {
			if (!isInScanRange(ref.getTypeName())) {
				continue;
			}
			if (!checkReference(ref)) {
				overallSuccess = false;
				missingReferences.add(ref);
			}
		}
		return overallSuccess;
	}

	private Set<TypeReference> collectReferences(InputStream classFileStream) throws FileNotFoundException,
			IOException {
		ReferenceCollector collector = new ReferenceCollector();
		Set<TypeReference> references = collector.analyze(classFileStream);
		return references;
	}

	private boolean checkReference(TypeReference ref) throws IOException {
		boolean verificationSuccess = verifier.verify(ref);
		if (!verificationSuccess && ref instanceof MethodReference) {

			MethodReference mref = (MethodReference) ref;

			Set<String> parentTypes = getParentTypes(ref.getTypeName());
			for (String typeName : parentTypes) {

				MethodReference newRef = new MethodReference(mref.getLine(), typeName, mref.getMethodName(),
						mref.getSignature());
				verificationSuccess = verifier.verify(newRef);
				if (verificationSuccess == true) {
					break;
				}
			}
		}
		return verificationSuccess;
	}

	private Set<String> getParentTypes(String className) throws IOException {
		InputStream stream = androidJar.openClass(className);
		if(stream == null) {
			return Collections.emptySet();
		}
		Set<String> parents = new HashSet<String>();
		try {
			ParentTypeCollector collector = new ParentTypeCollector();
			parents.addAll(collector.collect(stream));
		} finally {
			if (stream != null) {
				stream.close();
			}
		}
		Set<String> parentTypes = new HashSet<String>(parents);
		for (String typeName : parents) {
			parentTypes.addAll(getParentTypes(typeName));
		}
		return parentTypes;
	}

	public boolean isInScanRange(String typeName) {

		for (String scanPackage : scanPackages) {
			String replaced = scanPackage.replaceAll("\\.", "/");

			int lastIndexOf = typeName.lastIndexOf("/");
			// Ignore types of the default package
			if (lastIndexOf == -1) {
				return false;
			}
			String pkgName = typeName.substring(0, lastIndexOf);

			if (pkgName.equals(replaced)) {
				return true;
			}
		}

		return false;
	}
}
